 

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'

const props = defineProps({
  nodes: { type: Array, required: true },
  selectedNodeId: { type: [Number, null], required: true }
})

const emit = defineEmits([
  'selectNode',
  'moveNode',
  'dropNew'
])

const canvasRef = ref(null)
const wrapRef = ref(null)
const workWidth = ref(960)
const workHeight = ref(540)
const scale = ref(1)
const offsetX = ref(0)
const offsetY = ref(0)
const isPanning = ref(false)
const panStart = ref({ x: 0, y: 0, offsetX: 0, offsetY: 0 })
const resizingState = ref({
  isResizing: false,
  nodeId: null,
  startX: 0,
  startY: 0,
  startWidth: 0,
  startHeight: 0,
  startFontSize: 0
})

function onDragOver(event) {
  event.preventDefault()
}

function getCanvasPosition(event) {
  const rect = canvasRef.value.getBoundingClientRect()
  const localX = event.clientX - rect.left
  const localY = event.clientY - rect.top
  const contentX = (localX - offsetX.value) / scale.value
  const contentY = (localY - offsetY.value) / scale.value
  return { x: contentX, y: contentY, localX, localY }
}

function onDrop(event) {
  event.preventDefault()
  const type = event.dataTransfer.getData('application/x-type')
  const moveId = event.dataTransfer.getData('application/x-move-id')
  const { x, y } = getCanvasPosition(event)
  if (type) {
    emit('dropNew', { type, x: Math.round(x), y: Math.round(y) })
    return
  }
  if (moveId) {
    const offX = Number(event.dataTransfer.getData('application/x-offset-x') || 0)
    const offY = Number(event.dataTransfer.getData('application/x-offset-y') || 0)
    emit('moveNode', { id: Number(moveId), x: Math.max(0, Math.round(x - offX)), y: Math.max(0, Math.round(y - offY)) })
  }
}

function onCanvasClick(e) {
  if (e.target === canvasRef.value) emit('selectNode', null)
}

function onNodeDragStart(node, event) {
  event.dataTransfer.setData('application/x-move-id', String(node.id))
  const { x, y } = getCanvasPosition(event)
  event.dataTransfer.setData('application/x-offset-x', String(x - node.x))
  event.dataTransfer.setData('application/x-offset-y', String(y - node.y))
}

function onTextDblClick(node, event) {
  event.stopPropagation()
  const value = prompt('请输入文字内容', node.text)
  if (value !== null) node.text = value
}

function onResizeHandleDown(node, event) {
  event.stopPropagation()
  event.preventDefault()
  resizingState.value.isResizing = true
  resizingState.value.nodeId = node.id
  resizingState.value.startX = event.clientX
  resizingState.value.startY = event.clientY
  if (node.type === 'container') {
    resizingState.value.startWidth = node.width
    resizingState.value.startHeight = node.height
  } else if (node.type === 'text') {
    resizingState.value.startFontSize = node.fontSize
  }
  window.addEventListener('mousemove', onResizingMove)
  window.addEventListener('mouseup', onResizingUp)
}

function onResizingMove(event) {
  const state = resizingState.value
  if (!state.isResizing || state.nodeId == null) return
  const node = props.nodes.find(n => n.id === state.nodeId)
  if (!node) return
  const dx = event.clientX - state.startX
  const dy = event.clientY - state.startY
  if (node.type === 'container') {
    const newW = Math.max(10, state.startWidth + dx)
    const newH = Math.max(10, state.startHeight + dy)
    node.width = newW
    node.height = newH
  } else if (node.type === 'text') {
    const newSize = Math.max(8, Math.round(state.startFontSize + dy))
    node.fontSize = newSize
  }
}

function onResizingUp() {
  const state = resizingState.value
  if (!state.isResizing) return
  state.isResizing = false
  state.nodeId = null
  window.removeEventListener('mousemove', onResizingMove)
  window.removeEventListener('mouseup', onResizingUp)
}

function resizeWorkArea() {
  const wrap = wrapRef.value
  if (!wrap) return
  const availW = wrap.clientWidth
  const availH = wrap.clientHeight
  if (!availW || !availH) return
  // Fit 16:9 area into available space
  const targetW = Math.min(availW, Math.floor(availH * (16 / 9)))
  const targetH = Math.floor(targetW * (9 / 16))
  workWidth.value = targetW
  workHeight.value = targetH
}

function onWheel(event) {
  event.preventDefault()
  const delta = event.deltaY
  const zoomIntensity = 0.0015
  const rect = canvasRef.value.getBoundingClientRect()
  const mouseX = event.clientX - rect.left
  const mouseY = event.clientY - rect.top
  const prevScale = scale.value
  const nextScale = Math.min(3, Math.max(0.2, prevScale * (1 - delta * zoomIntensity)))
  if (nextScale === prevScale) return
  const contentX = (mouseX - offsetX.value) / prevScale
  const contentY = (mouseY - offsetY.value) / prevScale
  offsetX.value = mouseX - contentX * nextScale
  offsetY.value = mouseY - contentY * nextScale
  scale.value = nextScale
}

function onMouseDown(event) {
  if (event.button !== 2) return
  event.preventDefault()
  isPanning.value = true
  panStart.value.x = event.clientX
  panStart.value.y = event.clientY
  panStart.value.offsetX = offsetX.value
  panStart.value.offsetY = offsetY.value
  window.addEventListener('mousemove', onMouseMove)
  window.addEventListener('mouseup', onMouseUp)
}

function onMouseMove(event) {
  if (!isPanning.value) return
  const dx = event.clientX - panStart.value.x
  const dy = event.clientY - panStart.value.y
  offsetX.value = panStart.value.offsetX + dx
  offsetY.value = panStart.value.offsetY + dy
}

function onMouseUp() {
  if (!isPanning.value) return
  isPanning.value = false
  window.removeEventListener('mousemove', onMouseMove)
  window.removeEventListener('mouseup', onMouseUp)
}

function onContextMenu(event) {
  event.preventDefault()
}

function resetView() {
  scale.value = 1
  offsetX.value = 0
  offsetY.value = 0
}

onMounted(() => {
  resizeWorkArea()
  window.addEventListener('resize', resizeWorkArea)
})

onBeforeUnmount(() => {
  window.removeEventListener('mousemove', onResizingMove)
  window.removeEventListener('mouseup', onResizingUp)
  window.removeEventListener('resize', resizeWorkArea)
})
</script>

<template>
  <div class="canvas-wrap" ref="wrapRef">
    <button class="reset-btn" @click="resetView">重置视图</button>
    <main
      class="canvas"
      ref="canvasRef"
      :style="{ width: workWidth + 'px', height: workHeight + 'px', transform: 'translate(' + offsetX + 'px,' + offsetY + 'px) scale(' + scale + ')', transformOrigin: '0 0' }"
      @wheel="onWheel"
      @contextmenu="onContextMenu"
      @mousedown="onMouseDown"
      @dragover="onDragOver"
      @drop="onDrop"
      @click="onCanvasClick"
    >
      <div
      v-for="node in props.nodes"
      :key="node.id"
      class="node"
      :class="{ selected: node.id === props.selectedNodeId }"
      :style="{
        left: node.x + 'px',
        top: node.y + 'px',
        width: node.type === 'container' ? node.width + 'px' : 'auto',
        height: node.type === 'container' ? node.height + 'px' : 'auto',
        background: node.type === 'container' ? node.backgroundColor : undefined,
        border: node.type === 'container' ? (node.useBorder ? (node.borderWidth + 'px solid ' + node.borderColor) : 'none') : 'none',
        borderRadius: node.type === 'container' ? (node.borderRadius + 'px') : '0'
      }"
      draggable="true"
      @dragstart.stop="onNodeDragStart(node, $event)"
      @click.stop="emit('selectNode', node.id)"
      >
        <template v-if="node.type === 'container'"></template>
        <template v-else-if="node.type === 'text'">
          <span
            class="text-node"
            :style="{ color: node.color, fontSize: node.fontSize + 'px' }"
            @dblclick="onTextDblClick(node, $event)"
          >{{ node.text }}</span>
        </template>
        <div
          v-if="node.id === props.selectedNodeId"
          class="resize-handle"
          title="拖拽调整尺寸"
          @mousedown.prevent.stop="onResizeHandleDown(node, $event)"
        ></div>
      </div>
    </main>
  </div>
</template>

<style scoped>
.canvas-wrap {
  position: relative;
  width: 100%;
  height: 100%;
  background: #f3f4f6; /* 非工作区浅灰色 */
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 0; /* 画布容器在较低层，避免遮挡外部面板 */
}

.canvas {
  position: relative;
  background: #ffffff;
  overflow: hidden;
  box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 8px 24px rgba(0,0,0,0.06);
  z-index: 0; /* 主画布层级更低，由外部面板覆盖 */
}

.node {
  position: absolute;
  box-sizing: border-box;
  padding: 6px;
  background: rgba(255,255,255,0.9);
}

.node.selected {
  outline: 2px dashed #6366f1;
  outline-offset: 2px;
}

.text-node {
  white-space: pre;
}

.resize-handle {
  position: absolute;
  right: -6px;
  bottom: -6px;
  width: 12px;
  height: 12px;
  background: #6366f1;
  border: 2px solid #ffffff;
  border-radius: 3px;
  box-shadow: 0 1px 2px rgba(0,0,0,0.2);
  cursor: se-resize;
}

.reset-btn {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 1; /* 重置按钮不盖住外侧面板 */
  padding: 6px 10px;
  background: #ffffff;
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  box-shadow: 0 1px 2px rgba(0,0,0,0.06);
  cursor: pointer;
}
</style>


